package org.kca.yarmour.view;

import java.awt.Color;
import java.awt.Font;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.PriorityQueue;

import org.kca.graphics.Animation;
import org.kca.graphics.FontPalette;
import org.kca.graphics.GameCore;
import org.kca.graphics.IController;
import org.kca.graphics.Texture;
import org.kca.graphics.TextureLoader;
import org.kca.yarmour.*;
import org.kca.yarmour.data.*;
import org.kca.yarmour.data.GameState.TimeEvent;
import org.kca.yarmour.data.GlobalData.FactionType;
import org.kca.yarmour.data.GlobalData.TerrainType;
import org.kca.yarmour.data.GlobalData.CargoType;
import org.kca.yarmour.data.Quest.QuestStatus;
import org.kca.yarmour.utils.ImageUtils;
import org.kca.yarmour.utils.KcaGLUtils;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;

import static org.lwjgl.opengl.GL11.*;

public class MapScreen implements IController, TimeChangeListener, DialogListener, MouseAccess
{
    /** Pixels from edge of screen before map starts scrolling */
    private final static int SCROLL_THRESHOLD = 25;
    /** Map scroll speed in pixels per ms */
    private final static float SCROLL_SPEED = 0.08f; /* 80 pixels per second */
    private final static int CONTROL_PANEL_WIDTH = 160;
    private final static int GRID_TEXT_OFFSET_X = 20;
    private final static int GRID_TEXT_OFFSET_X2D = 24;
    private final static int GRID_TEXT_OFFSET_Y = 4;
    private final static int GRID_ROW_TEXT_THRESHOLD = 40;
    /** Amount of time before mouseover display shows */
    private final static long MAP_MOUSEOVER_THRESHOLD = 600;
    private final static int MOUSEOVER_MAP = -1;
    private final static int MOUSEOVER_TOOLTIP = 1;
    
    /* Control panel constants */
    private final static int ICON_SIZE = 32;
    private final static int MINI_ICON_SIZE = 16;
    private final static int ICON_ROW1_Y = 30;
    private final static int WEATHER_ICON_X = 8;
    private final static int TIDE_ICON_X = 40;
    private final static int PSI_ICON_X = 76;
    private final static int ICON_ROW2_Y = 72;
    private final static int COORDS_STR_X = 12;
    private final static int TERRAIN_ICON_X = 76;
    private final static int FACTION_ICON_X = 110;
    private final static int ICON_ROW3_Y = 112;
    private final static int ASEAN_ICON_X = 8;
    private final static int ASEAN_AMT_SPACING = 128;
    private final static int CP_DISPLAY_CARGO = 0;
    private final static int CP_DISPLAY_TITLE_Y = 494;
    private final static int CP_CARGO_ICON_X = 8;
    private final static int CP_CARGO_ICON_Y = 520;
    private final static int CP_CARGO_AMT_SPACING = 90;
    private final static int CP_CARGO_ICON_SPACING = 34;
    private final static int ICON_ROW4_Y = 152;
    private final static int ICON_ROW5_Y = 190;
    private final static int ACTION_BTN_X = 4;
    private final static int ACTION_BTN_SIZE = 36;
    private final static int ACTION_BTN_SPACING = 2;
    
    /* Notification box constants */
    private final static int NOTIFY_BOX_WIDTH = 490;
    private final static int NOTIFY_SCBAR_SIZE = 10;
    private final static int NOTIFY_BOX_HEIGHT = 96;
    private final static int NOTIFY_BOX_HEIGHT_BIG = 280;
    private final static int NOTIFY_BOX_LINES = 5;
    private final static int NOTIFY_BOX_LINES_BIG = 15;
    private final static int NOTIFY_SCBAR_DRAG_THRESHOLD = 100;
    private final static int NUM_NOTIFY_CATEGORIES = 3;
    private final static int NOTIFY_NEWS = 0;
    private final static int NOTIFY_QUEST = 1;
    private final static int NOTIFY_TRADE = 2;
    private final static int NOTIFY_TOGGLE_X = 452;
    private final static int NOTIFY_TOGGLE_BOX_SIZE = 10;
    private final static Color NOTIFY_BOX_BGCOLOR = new Color(255, 224, 224);
    private final static Color NOTIFY_BOX_BORDERCOLOR1 = new Color(128, 128, 128);
    private final static Color NOTIFY_BOX_BORDERCOLOR2 = Color.black;
    private final static Color NOTIFY_BOX_TEXT_COLOR = Color.black;
    private final static Color NEWS_NOTIFICATION_COLOR = Color.black;
    private final static Color QUEST_NOTIFICATION_COLOR = Color.blue;
    private final static Color TRADE_NOTIFICATION_COLOR = new Color(0, 192, 0);
    private final static Color NEWS_NOTIFICATION_TOGGLE_COLOR = NOTIFY_BOX_BORDERCOLOR1;
    
    /* Movement constants */
    private final static int[] DIROFF_X = {0, 0, -1, 1};
    private final static int[] DIROFF_Y = {-1, 1, 0, 0};
    /** Real movement time in ms, per hour of game time */
    private final static int PLAYER_MOVEMENT_TIME = 500;
    private static final Color COLOR_GRIDLINES = Color.black; //new Color(128, 128, 128);
    
    /* Game state references */
    private ArmourGame parent;
    private GameState gameState;
    
    /* Notification box variables */
    private int notifyX, notifyY, notifyDragX, notifyDragY, notifyLine;
    private int notifyScTopY, notifyScDragX, notifyScDragY;
    private boolean notifyDragged, notifyScrollDragged, notifyBoxBig;
    private NotificationList nlist;
    
    /* Dialog box variables */
    private PopupDialog dialog;
    private QuestListDialog qlistDialog;
    
    /* Map display variables */
    private Texture highTideIcon, lowTideIcon, psiIcon, aseanIcon, townIcon;
    private Texture laagerIcon, searchIcon, questIcon, hideIcon;
    private Texture supermarketIcon, workshopIcon, kopitiamIcon, facBranchIcon;
    private float viewX, viewY;
    private Texture mapImage;
    private FontPalette smallFont, notifyFont, defaultFont, boldFont;
    private int cpDisplayType;
    private boolean terrainOverlay, factionOverlay;
    private int mouseoverRow, mouseoverCol, mouseoverType, mouseoverIcon;
    private long mouseoverTime;
    
    /* Player sprite */
    private AnimatedSprite playerSprite;
    
    public MapScreen(ArmourGame parent, GameState gameState) throws YAException
    {
        this.parent = parent;
        this.gameState = gameState;
        smallFont = new FontPalette(new Font("Times New Roman", Font.PLAIN, 12));
        notifyFont = new FontPalette(new Font("Times New Roman", Font.PLAIN, 14));
        defaultFont = new FontPalette(new Font("Times New Roman", Font.PLAIN, 16));
        boldFont = new FontPalette(new Font("Times New Roman", Font.BOLD, 18));
        viewX = 0;
        viewY = 0;
        cpDisplayType = CP_DISPLAY_CARGO;
        terrainOverlay = false;
        factionOverlay = false;
        mouseoverTime = Long.MAX_VALUE;
        mouseoverType = 0;
        mouseoverIcon = -1;
        notifyX = CONTROL_PANEL_WIDTH;
        notifyY = parent.getHeight() - getNotifyBoxHeight();
        notifyDragged = false;
        notifyScrollDragged = false;
        notifyBoxBig = false;
        nlist = new NotificationList(NUM_NOTIFY_CATEGORIES);
        gameState.getGameTime().addTimeChangeListener(this);
        try
        {
            initImages();
            initPlayerSprite();
        }
        catch (IOException e)
        {
            throw new YAException(e);
        }
    }

    private void initImages() throws IOException
    {
        TextureLoader loader = GameCore.getTextureLoader();
        mapImage = loader.getTexture(GlobalData.MAP_FILENAME);
        highTideIcon = loader.getTexture(GlobalData.HIGH_TIDE_FILENAME);
        lowTideIcon = loader.getTexture(GlobalData.LOW_TIDE_FILENAME);
        psiIcon = loader.getTexture(GlobalData.PSI_FILENAME);
        aseanIcon = loader.getTexture(GlobalData.ASEAN_FILENAME);
        townIcon = loader.getTexture(GlobalData.TOWN_ICON_FILENAME);
        laagerIcon = loader.getTexture(GlobalData.LAAGER_ICON_FILENAME);
        searchIcon = loader.getTexture(GlobalData.SEARCH_ICON_FILENAME);
        hideIcon = loader.getTexture(GlobalData.HIDE_ICON_FILENAME);
        questIcon = loader.getTexture(GlobalData.QUEST_ICON_FILENAME);
        supermarketIcon = loader.getTexture(GlobalData.SUPERMARKET_ICON_FILENAME);
        workshopIcon = loader.getTexture(GlobalData.WORKSHOP_ICON_FILENAME);
        kopitiamIcon = loader.getTexture(GlobalData.KOPITIAM_ICON_FILENAME);
        facBranchIcon = loader.getTexture(GlobalData.FACBRANCH_ICON_FILENAME);
        
    }

    private void initPlayerSprite() throws IOException
    {
        playerSprite = new AnimatedSprite();
        TextureLoader loader = GameCore.getTextureLoader();
        final String[] spritePath = {"art/player/tankup", "art/player/tankdown", 
                "art/player/tankleft", "art/player/tankright"};
        for (int i = 0; i < 4; i++)
        {
            Animation anim = new Animation();
            for (int j = 0; j < 5; j++)
            {
                Texture tankImage = loader.getTexture(spritePath[i] + String.valueOf(j) + ".png");                    
                anim.addAnimation(tankImage, 250);
            }
            anim.makeCircular();
            playerSprite.addAnimation(anim);
        }
        Player p = gameState.getPlayer();
        playerSprite.setX(p.getCol() * GlobalData.SQUARE_SIZE);
        playerSprite.setY(p.getRow() * GlobalData.SQUARE_SIZE);
        playerSprite.setActiveAnimation(p.getFacing());
    }
    
    /* State methods */
    /**
     * Helper method to get the number of lines in the notify box.
     * @return the number of lines in the notify box
     */
    private int getNotifyBoxLines()
    {
        return notifyBoxBig ? NOTIFY_BOX_LINES_BIG : NOTIFY_BOX_LINES;
    }
    
    /**
     * Helper method to get the height of the notify box
     * @return the height in pixels of the box
     */
    private int getNotifyBoxHeight()
    {
        return notifyBoxBig ? NOTIFY_BOX_HEIGHT_BIG : NOTIFY_BOX_HEIGHT;
    }
    
    @Override
    public void drawComponent(int x, int y)
    {
        /* Draw main map */
        glPushMatrix();
        glTranslatef(CONTROL_PANEL_WIDTH, 0, 0);
        drawMap();
        drawOverlay();
        drawPlayer();
        glPopMatrix();
        drawControlPanel();
        drawActionPanel();
        drawNotificationBox();
        /* Draw mouseovers last */
        drawCPMouseover();
        glPushMatrix();
        glTranslatef(CONTROL_PANEL_WIDTH, 0, 0);
        drawMapMouseover();
        glPopMatrix();
        // Draw dialog box, if any
        if (dialog != null)
        {
            glColor4f(0, 0, 0, 0.5f);
            KcaGLUtils.fillRect(0, 0, parent.getWidth(), parent.getHeight());
            dialog.drawComponent(0, 0);
        }
    }
    
    private void drawMap()
    {
        int vx = Math.round(viewX);
        int vy = Math.round(viewY);
        KcaGLUtils.drawImage(mapImage, -vx, -vy);
    }
    
    /**
     * Draws the overlay of grid lines and other triggered overlays
     */
    private void drawOverlay()
    {
        int w = parent.getWidth() - CONTROL_PANEL_WIDTH;
        int h = parent.getHeight();
        int vx = Math.round(viewX);
        int vy = Math.round(viewY);
        int offx = GlobalData.SQUARE_SIZE - vx % GlobalData.SQUARE_SIZE;
        int offy = GlobalData.SQUARE_SIZE - vy % GlobalData.SQUARE_SIZE;
        WorldMap map = gameState.getWorldMap();
        // draw towns
        for (Town town : gameState.getWorldMap().getTowns())
        {
            int x = town.getLocation().getCol() * GlobalData.SQUARE_SIZE;
            int y = town.getLocation().getRow() * GlobalData.SQUARE_SIZE;
            KcaGLUtils.drawImage(townIcon, Math.round(x - viewX), Math.round(y - viewY));
        }
        if (terrainOverlay)
        {
            for (int i = offx - GlobalData.SQUARE_SIZE; i < w + GlobalData.SQUARE_SIZE; 
                    i += GlobalData.SQUARE_SIZE)
            {
                int cidx = (i + vx) / GlobalData.SQUARE_SIZE;
                if (cidx < 0 || cidx >= GlobalData.MAP_COLS) 
                {
                    continue;
                }
                for (int j = offy - GlobalData.SQUARE_SIZE; j < h + GlobalData.SQUARE_SIZE; 
                        j += GlobalData.SQUARE_SIZE)
                {
                    int ridx = (j + vy) / GlobalData.SQUARE_SIZE;
                    if (ridx < 0 || ridx >= GlobalData.MAP_ROWS)
                    {
                        continue;
                    }
                    WorldLocation wl = map.getLocation(ridx, cidx);
                    KcaGLUtils.drawImage(wl.getTerrain().getImage(), i, j);
                }
            }
        }
        /* Draw gridlines */
        KcaGLUtils.setColor(COLOR_GRIDLINES);
        for (int i = offx; i < w + GlobalData.SQUARE_SIZE; i += GlobalData.SQUARE_SIZE)
        {
            KcaGLUtils.drawLine(i, 0, i, h);
            int index = (i + vx)/GlobalData.SQUARE_SIZE;
            if (index < 10)
            {
                defaultFont.drawString(String.valueOf(index), i - GRID_TEXT_OFFSET_X, 
                        GRID_TEXT_OFFSET_Y);
            }
            else
            {
                defaultFont.drawString(String.valueOf(index), i - GRID_TEXT_OFFSET_X2D, 
                        GRID_TEXT_OFFSET_Y);
            }
        }
        KcaGLUtils.setColor(COLOR_GRIDLINES);
        for (int i = offy; i < h + GlobalData.SQUARE_SIZE; i += GlobalData.SQUARE_SIZE)
        {
            KcaGLUtils.drawLine(0, i, w, i);
            int index = (i + vy)/GlobalData.SQUARE_SIZE;
            if (i < GRID_ROW_TEXT_THRESHOLD)
            {
                /* Note, don't label first row so as not to overlap with first column */
                continue;
            }
            if (index < 10)
            {
                defaultFont.drawString(String.valueOf(index), 
                        GlobalData.SQUARE_SIZE - GRID_TEXT_OFFSET_X, 
                        i + GRID_TEXT_OFFSET_Y - GlobalData.SQUARE_SIZE);
            }
            else
            {
                defaultFont.drawString(String.valueOf(index),
                        GlobalData.SQUARE_SIZE - GRID_TEXT_OFFSET_X2D, 
                        i + GRID_TEXT_OFFSET_Y - GlobalData.SQUARE_SIZE);
            }
        }
        if (factionOverlay)
        {
            for (int i = offx - GlobalData.SQUARE_SIZE; i < w + GlobalData.SQUARE_SIZE; 
                    i += GlobalData.SQUARE_SIZE)
            {
                int cidx = (i + vx) / GlobalData.SQUARE_SIZE;
                if (cidx < 0 || cidx >= GlobalData.MAP_COLS) 
                {
                    continue;
                }
                for (int j = offy - GlobalData.SQUARE_SIZE; j < h + GlobalData.SQUARE_SIZE; 
                        j += GlobalData.SQUARE_SIZE)
                {
                    int ridx = (j + vy) / GlobalData.SQUARE_SIZE;
                    if (ridx < 0 || ridx >= GlobalData.MAP_ROWS)
                    {
                        continue;
                    }
                    WorldLocation wl = map.getLocation(ridx, cidx);
                    FactionType f = wl.getFaction();
                    if (f.equals(FactionType.NONE)) 
                    {
                        continue;
                    }
                    KcaGLUtils.setColor(wl.getFaction().getColor());
                    glLineWidth(3);
                    // draw 4 sides
                    if (cidx == 0 || map.getLocation(ridx, cidx - 1).getFaction() != f)
                    {
                        KcaGLUtils.drawLine(i + 2, j, i + 2, j - 1 + GlobalData.SQUARE_SIZE);
                    }
                    if (cidx == GlobalData.MAP_COLS - 1 || 
                            map.getLocation(ridx, cidx + 1).getFaction() != f)
                    {
                        KcaGLUtils.drawLine(i - 2 + GlobalData.SQUARE_SIZE, j, 
                                i - 2 + GlobalData.SQUARE_SIZE, j - 1+ GlobalData.SQUARE_SIZE);
                    }
                    if (ridx == 0 || map.getLocation(ridx - 1, cidx).getFaction() != f)
                    {
                        KcaGLUtils.drawLine(i, j + 2, i - 1 + GlobalData.SQUARE_SIZE, j + 2);
                    }
                    if (ridx == GlobalData.MAP_ROWS - 1 || 
                            map.getLocation(ridx + 1, cidx).getFaction() != f)
                    {
                        KcaGLUtils.drawLine(i, j - 2 + GlobalData.SQUARE_SIZE, 
                                i - 1 + GlobalData.SQUARE_SIZE, j - 2 + GlobalData.SQUARE_SIZE);
                    }
                    // fill in artifacts at corners
                    glLineWidth(1);
                    if (cidx == 0 || ridx == 0 || 
                            map.getLocation(ridx - 1, cidx - 1).getFaction() != f)
                    {
                        KcaGLUtils.fillRect(i, j, 3, 3);
                    }
                    if (cidx == GlobalData.MAP_COLS - 1 || ridx == 0 || 
                            map.getLocation(ridx - 1, cidx + 1).getFaction() != f)
                    {
                        KcaGLUtils.fillRect(i - 4 + GlobalData.SQUARE_SIZE, j, 3, 3);
                    }
                    if (cidx == 0 || ridx == GlobalData.MAP_ROWS - 1 || 
                            map.getLocation(ridx + 1, cidx - 1).getFaction() != f)
                    {
                        KcaGLUtils.fillRect(i, j - 4 + GlobalData.SQUARE_SIZE, 3, 3);
                    }
                    if (cidx == GlobalData.MAP_COLS - 1 || ridx == GlobalData.MAP_ROWS - 1 || 
                            map.getLocation(ridx + 1, cidx + 1).getFaction() != f)
                    {
                        KcaGLUtils.fillRect(i - 4 + GlobalData.SQUARE_SIZE, 
                                j - 4 + GlobalData.SQUARE_SIZE, 3, 3);
                    }
                }
            }
        }
    }
    
    /**
     * Draws the player sprite
     */
    private void drawPlayer()
    {
        /* TODO: Maybe I should do a synchronization check here, to ensure that the 
         * sprite is still in sync with the game state
         */
        playerSprite.drawComponent(Math.round(-viewX), Math.round(-viewY));
    }
    
    private void drawMapMouseover()
    {
        if (mouseoverType == MOUSEOVER_MAP)
        {
            WorldLocation wloc = gameState.getWorldMap().getLocation(mouseoverRow, mouseoverCol);
            if (wloc != null && wloc.getTown() != null)
            {
                // calculate position and size of mouseoverbox
                String townName = wloc.getTown().getName();
                int mbw = Math.max(ICON_SIZE * 2 , smallFont.stringWidth(townName)) + 4;
                int mbh = smallFont.getHeight() + ICON_SIZE + 3;
                /*
                 * Mouseover box defaults to align to right side, and extend down
                 * from top edge of square, unless there is not enough space
                 */
                int mbx = (mouseoverCol + 1) * GlobalData.SQUARE_SIZE - Math.round(viewX);
                if (mbx + mbw > parent.getWidth() - CONTROL_PANEL_WIDTH)
                {
                    mbx = (mouseoverCol * GlobalData.SQUARE_SIZE) - mbw - Math.round(viewX); 
                }
                int mby = mouseoverRow * GlobalData.SQUARE_SIZE - Math.round(viewY);
                if (mby + mbh > parent.getHeight())
                {
                    mby = (mouseoverRow + 1) * GlobalData.SQUARE_SIZE - mbh - Math.round(viewY);
                }
                glColor4f(0.95f, 0.95f, 0.7f, 1);
                KcaGLUtils.fillRect(mbx, mby, mbw, mbh);
                glColor4f(0, 0, 0, 1);
                glLineWidth(1);
                KcaGLUtils.drawRect(mbx, mby, mbw, mbh);
                smallFont.drawString(townName, mbx + 1, mby + 1);
                KcaGLUtils.drawScaledImage(wloc.getTerrain().getImage(), mbx + 1, mby + 1 + 
                        smallFont.getHeight(), ICON_SIZE, ICON_SIZE);
                KcaGLUtils.drawScaledImage(wloc.getFaction().getImage(), mbx + 2 + ICON_SIZE, 
                        mby + 1 + smallFont.getHeight(), ICON_SIZE, ICON_SIZE);
            }
            else
            {
             // calculate position and size of mouseoverbox
                int mbw = ICON_SIZE * 2 + 4;
                int mbh = ICON_SIZE + 3;
                /*
                 * Mouseover box defaults to align to right side, and extend down
                 * from top edge of square, unless there is not enough space
                 */
                int mbx = (mouseoverCol + 1) * GlobalData.SQUARE_SIZE - Math.round(viewX);
                if (mbx + mbw > parent.getWidth() - CONTROL_PANEL_WIDTH)
                {
                    mbx = (mouseoverCol * GlobalData.SQUARE_SIZE) - mbw - Math.round(viewX); 
                }
                int mby = mouseoverRow * GlobalData.SQUARE_SIZE - Math.round(viewY);
                if (mby + mbh > parent.getHeight())
                {
                    mby = (mouseoverRow + 1) * GlobalData.SQUARE_SIZE - mbh - Math.round(viewY);
                }
                glColor4f(0.95f, 0.95f, 0.7f, 1);
                KcaGLUtils.fillRect(mbx, mby, mbw, mbh);
                glColor4f(0, 0, 0, 1);
                glLineWidth(1);
                KcaGLUtils.drawRect(mbx, mby, mbw, mbh);
                KcaGLUtils.drawScaledImage(wloc.getTerrain().getImage(), mbx + 1, mby + 1, 
                        ICON_SIZE, ICON_SIZE);
                KcaGLUtils.drawScaledImage(wloc.getFaction().getImage(), mbx + 2 + ICON_SIZE, 
                        mby + 1, ICON_SIZE, ICON_SIZE);
            }
        }
    }
    
    /**
     * Draws the control panel
     */
    private void drawControlPanel()
    {
        glColor4f(1, 1, 1, 1);
        glLineWidth(1);
        KcaGLUtils.fillRect(0, 0, CONTROL_PANEL_WIDTH, parent.getHeight());
        glColor4f(0, 0, 0, 1);
        glLineWidth(3);
        KcaGLUtils.drawRect(0, 0, CONTROL_PANEL_WIDTH, parent.getHeight());
        glLineWidth(1);
        // draw time
        glColor4f(0, 0, 0, 1);
        defaultFont.drawString(gameState.getGameTime().toString(), 10, 4);
        // draw weather, tide and psi icons
        KcaGLUtils.drawImage(gameState.getWeather().getImage(), WEATHER_ICON_X, ICON_ROW1_Y);
        Texture tideIcon = gameState.getTide() >= GlobalData.HIGH_TIDE_THRESHOLD ? 
                highTideIcon : lowTideIcon;
        KcaGLUtils.drawImage(tideIcon, TIDE_ICON_X, ICON_ROW1_Y);
        KcaGLUtils.drawImage(psiIcon, PSI_ICON_X, ICON_ROW1_Y);
        // draw psi
        glColor4f(0, 0, 0, 1);
        boldFont.drawString(String.valueOf(gameState.getPsi()), PSI_ICON_X + 36, ICON_ROW1_Y + 4);
        // draw coordinates
        defaultFont.drawString(gameState.getPlayer().getCoordString(), COORDS_STR_X, ICON_ROW2_Y + 6);
        // draw terrain and faction icons
        Texture terrainIcon = gameState.getCurrentLocation().getTerrain().getImage();
        KcaGLUtils.drawImage(terrainIcon, TERRAIN_ICON_X, ICON_ROW2_Y);
        Texture factionIcon = gameState.getCurrentLocation().getFaction().getImage();
        KcaGLUtils.drawImage(factionIcon, FACTION_ICON_X, ICON_ROW2_Y);
        // draw money icon and value
        KcaGLUtils.drawImage(aseanIcon, ASEAN_ICON_X, ICON_ROW3_Y);
        String mStr = String.valueOf(gameState.getPlayer().getMoney());
        int lx = ImageUtils.rightAlignX(defaultFont, ASEAN_ICON_X + ASEAN_AMT_SPACING, mStr);
        glColor4f(0, 0, 0, 1);
        defaultFont.drawString(mStr, lx, ICON_ROW3_Y + 6);
        
        // Draw lower half of control panel display
        if (cpDisplayType == CP_DISPLAY_CARGO)
        {
            // draw cargo levels
            boldFont.drawString("Cargo", ImageUtils.centerAlignX(boldFont, 0, CONTROL_PANEL_WIDTH, 
                    "Cargo"), CP_DISPLAY_TITLE_Y);
            for (int i = 0; i < CargoType.getNumTypes(); i++)
            {
                KcaGLUtils.drawImage(CargoType.getCargoType(i).getImage(), CP_CARGO_ICON_X, 
                        CP_CARGO_ICON_Y + CP_CARGO_ICON_SPACING * i);
                String camt = String.valueOf(gameState.getPlayer().getCargo(i));
                glColor4f(0, 0, 0, 1);
                defaultFont.drawString(camt, ImageUtils.rightAlignX(defaultFont, CP_CARGO_ICON_X + 
                        CP_CARGO_AMT_SPACING, camt), CP_CARGO_ICON_Y + CP_CARGO_ICON_SPACING * i + 6);
            }
        }
    }

    /**
     * Draws the action buttons on the control panel
     */
    private void drawActionPanel()
    {
        // search / hide buttons
        KcaGLUtils.drawButton(ACTION_BTN_X, ICON_ROW4_Y, 
                ACTION_BTN_SIZE, ACTION_BTN_SIZE, searchIcon, KcaGLUtils.BUTTON_ENABLED);
        KcaGLUtils.drawButton(ACTION_BTN_X + ACTION_BTN_SIZE + ACTION_BTN_SPACING, ICON_ROW4_Y, 
                ACTION_BTN_SIZE, ACTION_BTN_SIZE, hideIcon, KcaGLUtils.BUTTON_ENABLED);
        KcaGLUtils.drawButton(ACTION_BTN_X + (ACTION_BTN_SIZE + ACTION_BTN_SPACING) * 2, ICON_ROW4_Y, 
                ACTION_BTN_SIZE, ACTION_BTN_SIZE, laagerIcon, KcaGLUtils.BUTTON_ENABLED);
        // quest button
        KcaGLUtils.drawButton(ACTION_BTN_X + (ACTION_BTN_SIZE + ACTION_BTN_SPACING) * 3, ICON_ROW4_Y, 
                ACTION_BTN_SIZE, ACTION_BTN_SIZE, questIcon, KcaGLUtils.BUTTON_ENABLED);
            
        if (gameState.getCurrentLocation().getTown() != null)
        {
            // draw town buttons
            KcaGLUtils.drawButton(ACTION_BTN_X, ICON_ROW5_Y, 
                    ACTION_BTN_SIZE, ACTION_BTN_SIZE, supermarketIcon, KcaGLUtils.BUTTON_ENABLED);
            KcaGLUtils.drawButton(ACTION_BTN_X + ACTION_BTN_SIZE + ACTION_BTN_SPACING, ICON_ROW5_Y, 
                    ACTION_BTN_SIZE, ACTION_BTN_SIZE, workshopIcon, KcaGLUtils.BUTTON_ENABLED);
            KcaGLUtils.drawButton(ACTION_BTN_X + (ACTION_BTN_SIZE + ACTION_BTN_SPACING) * 2, ICON_ROW5_Y, 
                    ACTION_BTN_SIZE, ACTION_BTN_SIZE, kopitiamIcon, KcaGLUtils.BUTTON_ENABLED);
            KcaGLUtils.drawButton(ACTION_BTN_X + (ACTION_BTN_SIZE + ACTION_BTN_SPACING) * 3, ICON_ROW5_Y, 
                    ACTION_BTN_SIZE, ACTION_BTN_SIZE, facBranchIcon, KcaGLUtils.BUTTON_ENABLED);
        }
        
    }
    
    /**
     * Draws a box for notifications
     */
    private void drawNotificationBox()
    {
        int mx = parent.getMouseX();
        int my = parent.getMouseY();
        int nx = notifyDragged ? notifyX + (mx - notifyDragX) : notifyX;
        int ny = notifyDragged ? notifyY + (my - notifyDragY) : notifyY;
        int nh = getNotifyBoxHeight();
        int nlines = getNotifyBoxLines();
        
        KcaGLUtils.setColor(NOTIFY_BOX_BGCOLOR);
        KcaGLUtils.fillRect(nx, ny, NOTIFY_BOX_WIDTH, nh);
        KcaGLUtils.setColor(NOTIFY_BOX_BORDERCOLOR1);
        KcaGLUtils.drawRect(nx, ny, NOTIFY_BOX_WIDTH, nh);
        KcaGLUtils.setColor(NOTIFY_BOX_BORDERCOLOR2);
        KcaGLUtils.drawRect(nx + 1, ny + 1, NOTIFY_BOX_WIDTH - 2, nh - 2);
        KcaGLUtils.drawLine(nx + NOTIFY_BOX_WIDTH - NOTIFY_SCBAR_SIZE, ny + 1, 
                nx + NOTIFY_BOX_WIDTH - NOTIFY_SCBAR_SIZE, ny + nh - 1);
        
        int nl = notifyLine;
        int nsize = nlist.getSize();
        int lineLimit = Math.min(nlines, nsize);
        int minSize = NOTIFY_SCBAR_SIZE / 2;
        boolean minSizeReached = nsize > Math.round(lineLimit / 
                (float)minSize * (nh - 2 * NOTIFY_SCBAR_SIZE - 1));
        if (notifyScrollDragged)
        {
            if (mx > notifyX + NOTIFY_BOX_WIDTH - NOTIFY_SCBAR_SIZE - 
                    NOTIFY_SCBAR_DRAG_THRESHOLD && mx < notifyX + NOTIFY_BOX_WIDTH + 
                    NOTIFY_SCBAR_DRAG_THRESHOLD)
            {
                int newtopy = notifyScTopY + (my - notifyScDragY);
                if (minSizeReached)
                {
                    nl = Math.round((newtopy - notifyY - NOTIFY_SCBAR_SIZE) / (float) 
                            (nh - 2 * NOTIFY_SCBAR_SIZE - 1 - minSize) *
                            (nsize - nlines));
                    nl = Math.min(nsize - lineLimit, Math.max(0, nl));
                }
                else
                {
                    nl = Math.round((newtopy - notifyY - NOTIFY_SCBAR_SIZE) / 
                            (float)(nh - 2 * NOTIFY_SCBAR_SIZE - 1) * nsize);
                    nl = Math.min(nsize - lineLimit, Math.max(0, nl));
                }
            }
        }
        KcaGLUtils.setColor(NOTIFY_BOX_TEXT_COLOR);
        for (int i = 0; i < lineLimit; i++)
        {
            NotificationList.Notification note = nlist.get(i + nl);
            KcaGLUtils.setColor(notifyCatColor(note.getCategory()));
            notifyFont.drawString(note.getData(), nx + 3, ny + 3 + 
                    i * (notifyFont.getHeight() + 1));
        }
        // draw scroll bar
        KcaGLUtils.setColor(NOTIFY_BOX_BORDERCOLOR1);
        int scbarx = nx + NOTIFY_BOX_WIDTH - NOTIFY_SCBAR_SIZE;
        
        if (nsize > 0)
        {
            if (minSizeReached)
            {
                int topy = Math.round(ny + NOTIFY_SCBAR_SIZE + nl / 
                        (float)(nsize - nlines) * 
                        (nh - 2 * NOTIFY_SCBAR_SIZE - 1 - minSize));
                KcaGLUtils.fillRect(scbarx, topy, NOTIFY_SCBAR_SIZE - 2, minSize);
            }
            else
            {
                int topy = Math.round(ny + NOTIFY_SCBAR_SIZE + nl / (float)nsize * 
                        (nh - 2 * NOTIFY_SCBAR_SIZE - 1));
                int scbarh = Math.round(lineLimit / (float)nsize * 
                        (nh - 2 * NOTIFY_SCBAR_SIZE - 1));
                KcaGLUtils.fillRect(scbarx, topy, NOTIFY_SCBAR_SIZE - 2, scbarh);
            }
        }
        else
        {
            KcaGLUtils.fillRect(scbarx, ny + NOTIFY_SCBAR_SIZE, NOTIFY_SCBAR_SIZE - 2, 
                    nh - 2 * NOTIFY_SCBAR_SIZE - 1);
        }
        
        KcaGLUtils.setColor(NOTIFY_BOX_BORDERCOLOR2);
        KcaGLUtils.drawLine(scbarx - 1, ny + NOTIFY_SCBAR_SIZE, scbarx + NOTIFY_SCBAR_SIZE - 1, 
                ny + NOTIFY_SCBAR_SIZE);
        KcaGLUtils.drawLine(scbarx - 1, ny + nh - NOTIFY_SCBAR_SIZE, 
                scbarx + NOTIFY_SCBAR_SIZE - 1, ny + nh - NOTIFY_SCBAR_SIZE);
        KcaGLUtils.drawLine(scbarx - 1, ny + NOTIFY_SCBAR_SIZE - 2, scbarx + NOTIFY_SCBAR_SIZE / 2 - 1, 
                ny + NOTIFY_SCBAR_SIZE / 2 - 2);
        KcaGLUtils.drawLine(scbarx - 1 + NOTIFY_SCBAR_SIZE / 2, ny + NOTIFY_SCBAR_SIZE / 2 - 2, 
                scbarx + NOTIFY_SCBAR_SIZE - 1, ny + NOTIFY_SCBAR_SIZE - 2);
        
        KcaGLUtils.drawLine(scbarx - 1, ny + nh - NOTIFY_SCBAR_SIZE + 1,
                scbarx + NOTIFY_SCBAR_SIZE / 2 - 1, ny + nh - NOTIFY_SCBAR_SIZE / 2 + 1);
        KcaGLUtils.drawLine(scbarx - 1 + NOTIFY_SCBAR_SIZE / 2, 
                ny + nh - NOTIFY_SCBAR_SIZE / 2 + 1, 
                scbarx + NOTIFY_SCBAR_SIZE - 1, ny + nh - NOTIFY_SCBAR_SIZE + 1);
        
        // draw buttons above scroll bar
        glColor4f(1, 1, 1, 1);
        KcaGLUtils.fillRect(nx + NOTIFY_TOGGLE_X, ny - NOTIFY_TOGGLE_BOX_SIZE, 
                NOTIFY_TOGGLE_BOX_SIZE, NOTIFY_TOGGLE_BOX_SIZE);
        KcaGLUtils.setColor(NEWS_NOTIFICATION_TOGGLE_COLOR);
        KcaGLUtils.fillRect(nx + NOTIFY_TOGGLE_X + NOTIFY_TOGGLE_BOX_SIZE - 1, 
                ny - NOTIFY_TOGGLE_BOX_SIZE, NOTIFY_TOGGLE_BOX_SIZE, NOTIFY_TOGGLE_BOX_SIZE);
        KcaGLUtils.setColor(QUEST_NOTIFICATION_COLOR.brighter());
        KcaGLUtils.fillRect(nx + NOTIFY_TOGGLE_X + NOTIFY_TOGGLE_BOX_SIZE * 2 - 2, 
                ny - NOTIFY_TOGGLE_BOX_SIZE, NOTIFY_TOGGLE_BOX_SIZE, NOTIFY_TOGGLE_BOX_SIZE);
        KcaGLUtils.setColor(TRADE_NOTIFICATION_COLOR.brighter());
        KcaGLUtils.fillRect(nx + NOTIFY_TOGGLE_X + NOTIFY_TOGGLE_BOX_SIZE * 3 - 3, 
                ny - NOTIFY_TOGGLE_BOX_SIZE, NOTIFY_TOGGLE_BOX_SIZE, NOTIFY_TOGGLE_BOX_SIZE);
        glColor4f(0, 0, 0, 1);
        KcaGLUtils.drawRect(nx + NOTIFY_TOGGLE_X, ny - NOTIFY_TOGGLE_BOX_SIZE, 
                NOTIFY_TOGGLE_BOX_SIZE, NOTIFY_TOGGLE_BOX_SIZE);
        KcaGLUtils.drawRect(nx + NOTIFY_TOGGLE_X + NOTIFY_TOGGLE_BOX_SIZE - 1, 
                ny - NOTIFY_TOGGLE_BOX_SIZE, NOTIFY_TOGGLE_BOX_SIZE, NOTIFY_TOGGLE_BOX_SIZE);
        KcaGLUtils.drawRect(nx + NOTIFY_TOGGLE_X + NOTIFY_TOGGLE_BOX_SIZE * 2 - 2, 
                ny - NOTIFY_TOGGLE_BOX_SIZE, NOTIFY_TOGGLE_BOX_SIZE, NOTIFY_TOGGLE_BOX_SIZE);
        KcaGLUtils.drawRect(nx + NOTIFY_TOGGLE_X + NOTIFY_TOGGLE_BOX_SIZE * 3 - 3, 
                ny - NOTIFY_TOGGLE_BOX_SIZE, NOTIFY_TOGGLE_BOX_SIZE, NOTIFY_TOGGLE_BOX_SIZE);
    }
    
    /**
     * Draws the tooltip box when mouseovering a control panel button
     */
    private void drawCPMouseover()
    {
        if (mouseoverType == MOUSEOVER_TOOLTIP)
        {
            switch (mouseoverIcon)
            {
            case 1:
                KcaGLUtils.drawTooltip(ACTION_BTN_X + ACTION_BTN_SIZE * 1, 
                        ICON_ROW4_Y, smallFont, "Search (R)");
                break;
            case 2:
                KcaGLUtils.drawTooltip(ACTION_BTN_X + ACTION_BTN_SIZE * 2 + ACTION_BTN_SPACING * 1, 
                        ICON_ROW4_Y, smallFont, "Hide (H)");
                break;
            case 3:
                KcaGLUtils.drawTooltip(ACTION_BTN_X + ACTION_BTN_SIZE * 3 + ACTION_BTN_SPACING * 2, 
                        ICON_ROW4_Y, smallFont, "Laager (L)");
                break;
            case 4:
                // only draw if secret location is on
                if (gameState.onSecretLocation())
                {
                    KcaGLUtils.drawTooltip(ACTION_BTN_X + ACTION_BTN_SIZE * 4 + ACTION_BTN_SPACING * 3, 
                            ICON_ROW4_Y, smallFont, "Quest (Q)");
                }
                break;
            case 5:
                if (gameState.onTown())
                {
                    KcaGLUtils.drawTooltip(ACTION_BTN_X + ACTION_BTN_SIZE * 1, 
                            ICON_ROW5_Y, smallFont, "Supermarket (1)");
                }
                break;
            case 6:
                if (gameState.onTown())
                {
                    KcaGLUtils.drawTooltip(ACTION_BTN_X + ACTION_BTN_SIZE * 2 + ACTION_BTN_SPACING * 1, 
                            ICON_ROW5_Y, smallFont, "Workshop (2)");
                }
                break;
            case 7:
                if (gameState.onTown())
                {
                    KcaGLUtils.drawTooltip(ACTION_BTN_X + ACTION_BTN_SIZE * 3 + ACTION_BTN_SPACING * 2, 
                            ICON_ROW5_Y, smallFont, "Kopitiam (3)");
                }
                break;
            case 8:
                if (gameState.onTown())
                {
                    KcaGLUtils.drawTooltip(ACTION_BTN_X + ACTION_BTN_SIZE * 4 + ACTION_BTN_SPACING * 3, 
                            ICON_ROW5_Y, smallFont, "Faction Branch (4)");
                }
                break;
            }
        }
    }
    
    @Override
    public void update(long elapsedTime)
    {
        checkMouseover(elapsedTime);
        checkMapScroll(elapsedTime);
        playerSprite.update(elapsedTime);
        gameState.getGameTime().update(elapsedTime);
    }
    
    /**
     * Checks the position of the mouse to decide if the mouseover info box 
     * should be displayed
     * @param elapsedTime the time elapsed in ms since the last time update was called
     */
    private void checkMouseover(long elapsedTime)
    {
        int mx = parent.getMouseX();
        int my = parent.getMouseY();
        if (dialog != null)
        {
            /* Don't show mouseovers if a dialog box is showing */
            return;
        }
        
        if (notifyDragged || (mx >= notifyX && mx < notifyX + NOTIFY_BOX_WIDTH && 
                my >= notifyY && my < notifyY + getNotifyBoxHeight()))
        {
            /* Don't show mouseovers if mouse is on the notification box */
            mouseoverType = 0;
            return;
        }
        if (my >= notifyY - NOTIFY_TOGGLE_BOX_SIZE && my < notifyY &&
                mx >= notifyX + NOTIFY_TOGGLE_X && mx <= notifyX + NOTIFY_TOGGLE_X + 
                (NOTIFY_TOGGLE_BOX_SIZE - 1) * 4)
        {
            /* Don't show mouseovers if mouse is on the toggle buttons above
             * the notification box */
            mouseoverType = 0;
            return;
        }
        
        else if (mx >= CONTROL_PANEL_WIDTH)
        {
            // mouse is over map
            mouseoverIcon = -1;
            int tc = (int)Math.floor((mx + viewX - CONTROL_PANEL_WIDTH) / GlobalData.SQUARE_SIZE);
            int tr = (int)Math.floor((my + viewY) / GlobalData.SQUARE_SIZE);
            if (tc == mouseoverCol && tr == mouseoverRow)
            {
                if (mouseoverType != MOUSEOVER_MAP)
                {
                    long mot = GameCore.getTime() - mouseoverTime;
                    if (mot > MAP_MOUSEOVER_THRESHOLD)
                    {
                        /*
                         * Mouseover check: don't update if mouse is over a panel or
                         * out of the screen
                         */
                        if (tr >= 0 && tr < GlobalData.MAP_ROWS && tc >= 0 && tc < GlobalData.MAP_COLS)
                        {
                            mouseoverType = MOUSEOVER_MAP;
                        }
                    }
                }
            }
            else
            {
                mouseoverRow = tr;
                mouseoverCol = tc;
                mouseoverTime = GameCore.getTime();
                mouseoverType = 0;
            }
        }
        else
        {
            // mouse is over control panel
            int ticon = -1;
            if (my >= ICON_ROW4_Y && my < ICON_ROW4_Y + ACTION_BTN_SIZE)
            {
                for (int i = 0; i < 4; i++)
                {
                    int btx = ACTION_BTN_X + (ACTION_BTN_SIZE + ACTION_BTN_SPACING) * i;
                    if (mx >= btx && mx < btx + ACTION_BTN_SIZE)
                    {
                        ticon = (i+1);
                        break;
                    }
                }
            }
            if (my >= ICON_ROW5_Y && my < ICON_ROW5_Y + ACTION_BTN_SIZE)
            {
                for (int i = 0; i < 4; i++)
                {
                    int btx = ACTION_BTN_X + (ACTION_BTN_SIZE + ACTION_BTN_SPACING) * i;
                    if (mx >= btx && mx < btx + ACTION_BTN_SIZE)
                    {
                        ticon = (i+5);
                        break;
                    }
                }
            }
            if (ticon == mouseoverIcon && ticon > 0)
            {
                long mot = GameCore.getTime() - mouseoverTime;
                if (mot > MAP_MOUSEOVER_THRESHOLD)
                {
                    mouseoverType = MOUSEOVER_TOOLTIP;
                }
            }
            else
            {
                mouseoverTime = GameCore.getTime();
                mouseoverIcon = ticon;
                mouseoverType = 0;
            }
        }
    }
    
    /**
     * Checks mouse position to decide whether to scroll the map
     * @param elapsedTime the time elapsed in ms since the last time update was called
     */
    private void checkMapScroll(long elapsedTime)
    {
        int mx = parent.getMouseX();
        int my = parent.getMouseY();
        if (dialog != null)
        {
            /* Don't scroll the map if a dialog box is showing */
            return;
        }
        
        // map scroll
        if (mx < SCROLL_THRESHOLD)
        {
            viewX -= SCROLL_SPEED * elapsedTime;
            if (viewX <= 0) viewX = 0;
        }
        if (mx > parent.getWidth() - SCROLL_THRESHOLD)
        {
            viewX += SCROLL_SPEED * elapsedTime;
            if (viewX > GlobalData.MAP_WIDTH - parent.getWidth() + CONTROL_PANEL_WIDTH)
            {
                viewX = GlobalData.MAP_WIDTH - parent.getWidth() + CONTROL_PANEL_WIDTH;
            }
        }
        if (my < SCROLL_THRESHOLD)
        {
            viewY -= SCROLL_SPEED * elapsedTime;
            if (viewY <= 0) viewY = 0;
        }
        if (my > parent.getHeight() - SCROLL_THRESHOLD)
        {
            viewY += SCROLL_SPEED * elapsedTime;
            if (viewY > GlobalData.MAP_HEIGHT - parent.getHeight())
            {
                viewY = GlobalData.MAP_HEIGHT - parent.getHeight();
            }
        }
    }
    
    /**
     * Centers the map on player
     */
    private void centerMapOnPlayer()
    {
        WorldLocation wloc = gameState.getCurrentLocation();
        int ivx = (wloc.getCol() * GlobalData.SQUARE_SIZE) + 
                (GlobalData.SQUARE_SIZE - parent.getWidth() + CONTROL_PANEL_WIDTH) / 2;
        int ivy = wloc.getRow() * GlobalData.SQUARE_SIZE + 
                (GlobalData.SQUARE_SIZE - parent.getHeight())/ 2;
        if (ivx <= 0) ivx = 0;
        if (ivx > GlobalData.MAP_WIDTH - parent.getWidth() + CONTROL_PANEL_WIDTH)
        {
            ivx = GlobalData.MAP_WIDTH - parent.getWidth() + CONTROL_PANEL_WIDTH;
        }
        if (ivy <= 0) ivy = 0;
        if (ivy > GlobalData.MAP_HEIGHT - parent.getHeight())
        {
            ivy = GlobalData.MAP_HEIGHT - parent.getHeight();
        }
        viewX = ivx;
        viewY = ivy;
    }

    /**
     * Called when move is pressed
     * @param direction the direction moved, must be one of the cardinal directions
     * defined.
     */
    private void movePressed(int direction)
    {
        /* Ignore if sprite is already moving */
        if (playerSprite.isMoving())
        {
            return;
        }
        Player p = gameState.getPlayer();
        /* Check whether new cell is passable */
        int tr = p.getRow() + DIROFF_Y[direction];
        int tc = p.getCol() + DIROFF_X[direction];
        TerrainType ttype = gameState.getWorldMap().getLocation(tr, tc).getTerrain();
        if (tr < 0 || tr >= GlobalData.MAP_ROWS || tc < 0 || tc >= GlobalData.MAP_COLS)
        {
            return;
        }
        if (ttype.getTravelTime() <= 0)
        {
            // TODO: implement special terrain checks here
            return;
        }
        /* Update game state */
        p.setFacing(direction);
        gameState.movePlayer(tr, tc);
        /* Update sprite */
        playerSprite.setActiveAnimation(direction);
        long moveTime = PLAYER_MOVEMENT_TIME * ttype.getTravelTime();
        double vx = DIROFF_X[direction] * GlobalData.SQUARE_SIZE / (double)moveTime;
        double vy = DIROFF_Y[direction] * GlobalData.SQUARE_SIZE / (double)moveTime;
        playerSprite.startMovement(vx, vy, moveTime);
        /* Update game time */
        gameState.getGameTime().advanceGameTime(GameTime.GAME_HOUR, ttype.getTravelTime(), 
                PLAYER_MOVEMENT_TIME);
    }
    
    /**
     * Toggles the terrain overlay
     */
    private void terrainIconPressed()
    {
        terrainOverlay = !terrainOverlay;
    }
    
    /**
     * Toggles the faction overlay
     */
    private void factionIconPressed()
    {
        factionOverlay = !factionOverlay;
    }
    
    /**
     * Searches the area on the world map. This consumes time dependent
     * on the terrain
     */
    private void searchPressed()
    {
        if (gameState.getCurrentLocation().getTown() == null)
        {
            // can only search when not in town
        }
    }
    
    /**
     * Hides on the map. This consumes 3 hours of game time.
     */
    private void hidePressed()
    {
        if (gameState.getCurrentLocation().getTown() == null)
        {
            // can only hide when not in town
        }
    }
    
    /**
     * Enters a laager. This consumes 12 hours of game time.
     */
    private void laagerPressed()
    {

    }
    
    /**
     * Triggers the quest panel
     */
    private void questPressed()
    {
        showQuestListDialog();
    }
    
    /**
     * Enters the supermarket and opens the trade dialog
     */
    private void supermarketPressed()
    {
        Town town = gameState.getCurrentLocation().getTown();
        if (town == null)
        {
            return;
        }
        showTradeDialog(town);
    }

    private void workshopPressed()
    {
        
    }
    
    private void kopitiamPressed()
    {
        
    }
    
    private void facBranchPressed()
    {
        
    }
    
    /**
     * Toggles the notification box between small and large size
     */
    private void toggleNotifyBoxSize()
    {
        if (!notifyBoxBig)
        {
            // expanding notify box size
            notifyLine = Math.max(0, Math.min(notifyLine, nlist.getSize() - NOTIFY_BOX_LINES_BIG));
        }
        notifyBoxBig = !notifyBoxBig;
    }
    
    /**
     * Toggles whether a category is to be displayed in the notification box
     * @param category the category to toggle
     */
    private void toggleCategoryPressed(int category)
    {
        int tindex = nlist.getTrueIndex(notifyLine);
        nlist.toggleFilter(category);
        if (tindex == -1)
        {
            notifyLine = 0;
        }
        else
        {
            notifyLine = nlist.getValidIndex(tindex);
            int nsize = nlist.getSize();
            int lineLimit = Math.min(getNotifyBoxLines(), nsize);
            notifyLine = Math.min(nsize - lineLimit, notifyLine);
        }
    }
    
    /**
     * Adds a notification to the notification list
     * @param s the notification to add
     * @param category the category of the notification
     */
    private void addNotification(String s, int category)
    {
        int nsize = nlist.getSize();
        int nlines = getNotifyBoxLines();
        boolean atBottom = notifyLine == Math.max(nsize - nlines, 0);
        List<String> toadd = ImageUtils.wrapString(s, 
                NOTIFY_BOX_WIDTH - NOTIFY_SCBAR_SIZE - 3, notifyFont, true);
        for (int i = 0; i < toadd.size(); i++)
        {
            nlist.addNotification(toadd.get(i), category);
        }
        if (atBottom)
        {
            notifyLine = Math.max(nlist.getSize() - nlines, 0);
        }
    }

    /**
     * Scrolls the notification text. Specify 1 or -1 to scroll 1 line forward or backward,
     * and 2 or -2 to scroll many lines forward or backward
     * @param amount the amount to scroll
     */
    private void scrollNotifyText(int amount)
    {
        int nsize = nlist.getSize();
        int nlines = getNotifyBoxLines();
        if (nsize <= nlines)
        {
            return;
        }
        switch (amount)
        {
        case -2:
            notifyLine = Math.max(0, notifyLine - nlines);
            break;
        case -1:
            notifyLine = Math.max(0, notifyLine - 1);
            break;
        case 0:
            break;
        case 1:
            notifyLine = Math.min(nsize - nlines, notifyLine + 1);
            break;
        case 2:
            notifyLine = Math.min(nsize - nlines, notifyLine + nlines);
            break;
        }
    }
    
    /**
     * Shows the trade dialog in the specified town
     * @param town the town to open the supermarket trade dialog in
     */
    private void showTradeDialog(Town town)
    {
        TradeDialog td = new TradeDialog();
        td.setDialogListener(this);
        td.setX(200);
        td.setY(150);
        td.initTradeParameters(town, gameState.getPlayer());
        td.setMouseAccessProvider(this);
        dialog = td;
    }
    
    /**
     * Shows the quest dialog for the current location
     */
    private void showQuestListDialog()
    {
        if (qlistDialog == null)
        {
            qlistDialog = new QuestListDialog();
            qlistDialog.initQuestDialog(gameState.getPlayer());
            qlistDialog.setMouseAccessProvider(this);
        }
        qlistDialog.setDialogListener(this);
        qlistDialog.setX(200);
        qlistDialog.setY(100);
        qlistDialog.initAvailableQuests(gameState.getCurrentLocation().getAvailableQuests());
        qlistDialog.questsUpdated();
        dialog = qlistDialog;
    }

    @Override
    public void timeChanged(Calendar oldTime, Calendar newTime)
    {
        PriorityQueue<TimeEvent> pq = gameState.getEventQueue();
        while (!pq.isEmpty())
        {
            TimeEvent evt = pq.peek();
            if (evt.getTime().compareTo(newTime) > 0)
            {
                break;
            }            
            pq.poll();
            switch(evt.getEventCode())
            {
            case GlobalData.EVENT_WEATHER_CHANGE:
                String note = gameState.changeWeather();
                Calendar nextTime = (Calendar)gameState.getGameTime().getTime().clone();
                nextTime.add(Calendar.HOUR, GlobalData.WEATHER_CHANGE_HOURS);
                pq.add(new TimeEvent(nextTime, GlobalData.EVENT_WEATHER_CHANGE));
                addNotification(note, NOTIFY_NEWS);
                break;
            }
        }
    }
    
    @Override
    public void mousePressed(int button, int mx, int my)
    {
        if (dialog != null)
        {
            dialog.mousePressed(button, mx - dialog.getX(), my - dialog.getY());
            return;
        }
        
        if (button == 0) // left click
        {
            int nh = getNotifyBoxHeight();
            int nlines = getNotifyBoxLines();
            if (mx >= notifyX && mx < notifyX + NOTIFY_BOX_WIDTH && 
                    my >= notifyY && my < notifyY + nh)
            {
                // mouse is in notification box
                if (mx > notifyX + NOTIFY_BOX_WIDTH - NOTIFY_SCBAR_SIZE)
                {
                    // mouse is on scroll bar
                    int nsize = nlist.getSize();
                    if (nsize <= nlines)
                    {
                        return;
                    }
                    int minSize = NOTIFY_SCBAR_SIZE / 2;
                    int lineLimit = Math.min(nlines, nsize);
                    int topy, scbarh = minSize;
                    if (nsize > Math.round(lineLimit / (float)minSize * 
                            (nh - 2 * NOTIFY_SCBAR_SIZE - 1)))
                    {
                        topy = Math.round(notifyY + NOTIFY_SCBAR_SIZE + notifyLine / 
                                (float)(nsize - nlines) * 
                                (nh - 2 * NOTIFY_SCBAR_SIZE - 1 - minSize));
                    }
                    else
                    {
                        topy = Math.round(notifyY + NOTIFY_SCBAR_SIZE + 
                                notifyLine / (float)nsize * 
                                (nh - 2 * NOTIFY_SCBAR_SIZE - 1));
                        scbarh = Math.round(lineLimit / (float)nsize * 
                                (nh - 2 * NOTIFY_SCBAR_SIZE - 1));
                    }
                    /*
                    int topy = Math.round(notifyY + NOTIFY_SCBAR_SIZE + notifyLine / 
                            (float)notifyList.size() * (nh - 2 * NOTIFY_SCBAR_SIZE - 1));
                    int scbarh = Math.round(lineLimit / (float)notifyList.size() * 
                            (nh - 2 * NOTIFY_SCBAR_SIZE - 1));
                    */
                    if (my < notifyY + NOTIFY_SCBAR_SIZE)
                    {
                        scrollNotifyText(-1);
                    }
                    else if (my < topy)
                    {
                        scrollNotifyText(-2);
                    }
                    else if (my <= topy + scbarh)
                    {
                        notifyScrollDragged = true;
                        notifyScDragX = mx;
                        notifyScDragY = my;
                        notifyScTopY = topy;
                    }
                    else if (my < notifyY + nh - NOTIFY_SCBAR_SIZE)
                    {
                        scrollNotifyText(2);
                    }
                    else
                    {
                        scrollNotifyText(1);
                    }
                }
                else
                {
                    // drag
                    notifyDragged = true;
                    notifyDragX = mx;
                    notifyDragY = my;
                }
                // return here so that other components will not be affected
                return;
            }
            else if (my >= notifyY - NOTIFY_TOGGLE_BOX_SIZE && my < notifyY &&
                    mx >= notifyX + NOTIFY_TOGGLE_X && mx <= notifyX + NOTIFY_TOGGLE_X + 
                    (NOTIFY_TOGGLE_BOX_SIZE - 1) * 4)
            {
                // mouse clicked inside boxes
                int mpos = (mx - notifyX - NOTIFY_TOGGLE_X) / (NOTIFY_TOGGLE_BOX_SIZE - 1);
                switch (mpos)
                {
                case 0:
                    toggleNotifyBoxSize();
                    break;
                case 1:
                    toggleCategoryPressed(NOTIFY_NEWS);
                    break;
                case 2:
                    toggleCategoryPressed(NOTIFY_QUEST);
                    break;
                case 3:
                    toggleCategoryPressed(NOTIFY_TRADE);
                    break;
                }
            }
            else if (my >= ICON_ROW2_Y && my < ICON_ROW2_Y + ICON_SIZE)
            {
                if (mx < TERRAIN_ICON_X && mx > COORDS_STR_X)
                {
                    centerMapOnPlayer();
                }
                else if (mx >= TERRAIN_ICON_X && mx < TERRAIN_ICON_X + ICON_SIZE)
                {
                    terrainIconPressed();
                }
                else if (mx >= FACTION_ICON_X && mx < FACTION_ICON_X + ICON_SIZE)
                {
                    factionIconPressed();
                }
            }
            /* For rest of LMB commands, ignore if sprite is already moving */
            if (playerSprite.isMoving())
            {
                return;
            }
            if (my > ICON_ROW4_Y && my < ICON_ROW4_Y + ACTION_BTN_SIZE)
            {
                if (mx >= ACTION_BTN_X && mx < ACTION_BTN_X + ACTION_BTN_SIZE)
                {
                    searchPressed();
                }
                else if (mx >= ACTION_BTN_X + ACTION_BTN_SIZE + ACTION_BTN_SPACING &&
                        mx < ACTION_BTN_X + ACTION_BTN_SIZE * 2 + ACTION_BTN_SPACING)
                {
                    hidePressed();
                }
                else if (mx >= ACTION_BTN_X + ACTION_BTN_SIZE * 2 + 2 * ACTION_BTN_SPACING &&
                        mx < ACTION_BTN_X + ACTION_BTN_SIZE * 3 + 2 * ACTION_BTN_SPACING)
                {
                    laagerPressed();
                }
                else if (mx >= ACTION_BTN_X + ACTION_BTN_SIZE * 3 + 3 * ACTION_BTN_SPACING &&
                        mx < ACTION_BTN_X + ACTION_BTN_SIZE * 4 + 3 * ACTION_BTN_SPACING)
                {
                    questPressed();
                }
            }
            else if (my > ICON_ROW5_Y && my < ICON_ROW5_Y + ACTION_BTN_SIZE && gameState.onTown())
            {
                if (mx >= ACTION_BTN_X && mx < ACTION_BTN_X + ACTION_BTN_SIZE)
                {
                    supermarketPressed();
                }
                else if (mx >= ACTION_BTN_X + ACTION_BTN_SIZE + ACTION_BTN_SPACING &&
                        mx < ACTION_BTN_X + ACTION_BTN_SIZE * 2 + ACTION_BTN_SPACING)
                {
                    workshopPressed();
                }
                else if (mx >= ACTION_BTN_X + ACTION_BTN_SIZE * 2 + 2 * ACTION_BTN_SPACING &&
                        mx < ACTION_BTN_X + ACTION_BTN_SIZE * 3 + 2 * ACTION_BTN_SPACING)
                {
                    kopitiamPressed();
                }
                else if (mx >= ACTION_BTN_X + ACTION_BTN_SIZE * 3 + 3 * ACTION_BTN_SPACING &&
                        mx < ACTION_BTN_X + ACTION_BTN_SIZE * 4 + 3 * ACTION_BTN_SPACING)
                {
                    facBranchPressed();
                }
            }
        }
        else if (button == 1) // right click
        {
            if (mx <= CONTROL_PANEL_WIDTH)
            {
                
            }
            else
            {
                // clicked on map, place checks for overlays here
                int tc = (int)Math.floor((mx + viewX - CONTROL_PANEL_WIDTH) / GlobalData.SQUARE_SIZE);
                int tr = (int)Math.floor((my + viewY) / GlobalData.SQUARE_SIZE);
                int cr = gameState.getCurrentLocation().getRow();
                int cc = gameState.getCurrentLocation().getCol();
                if (tr < 0 || tr >= GlobalData.MAP_ROWS || tc < 0 || tc >= GlobalData.MAP_COLS ||
                        (tc == cc && tr == cr))
                {
                    return;
                }
                if (Math.abs(cc - tc) < Math.abs(cr - tr))
                {
                    if (tr < cr)
                    {
                        movePressed(GlobalData.DIR_UP);
                    }
                    else
                    {
                        movePressed(GlobalData.DIR_DOWN);
                    }
                }
                else
                {
                    if (tc < cc)
                    {
                        movePressed(GlobalData.DIR_LEFT);
                    }
                    else
                    {
                        movePressed(GlobalData.DIR_RIGHT);
                    }
                }
                
            }
        }
    }
    
    @Override
    public void mouseReleased(int button, int mx, int my)
    {
        if (dialog != null)
        {
            dialog.mouseReleased(button, mx - dialog.getX(), my - dialog.getY());
            return;
        }
        
        if (button == 0)
        {
            if (notifyDragged)
            {
                notifyX += (mx - notifyDragX);
                notifyY += (my - notifyDragY);
                notifyDragged = false;
                return;
            }
            if (notifyScrollDragged)
            {            
                if (mx < notifyX + NOTIFY_BOX_WIDTH - NOTIFY_SCBAR_SIZE - NOTIFY_SCBAR_DRAG_THRESHOLD ||
                        mx > notifyX + NOTIFY_BOX_WIDTH + NOTIFY_SCBAR_DRAG_THRESHOLD)
                {
                    return;
                }
                int nh = getNotifyBoxHeight();
                int nlines = getNotifyBoxLines();
                int nsize = nlist.getSize();
                int lineLimit = Math.min(nlines, nsize);
                int minSize = NOTIFY_SCBAR_SIZE / 2;
                boolean minSizeReached = nsize > Math.round(lineLimit / (float)minSize * 
                        (nh - 2 * NOTIFY_SCBAR_SIZE - 1));
                int newtopy = notifyScTopY + (my - notifyScDragY);
                if (minSizeReached)
                {
                    notifyLine = Math.round((newtopy - notifyY - NOTIFY_SCBAR_SIZE) / (float) 
                            (nh - 2 * NOTIFY_SCBAR_SIZE - 1 - minSize) *
                            (nsize - nlines));
                    notifyLine = Math.min(nsize - lineLimit, Math.max(0, notifyLine));
                }
                else
                {
                    notifyLine = Math.round((newtopy - notifyY - NOTIFY_SCBAR_SIZE) / 
                            (float) (nh - 2 * NOTIFY_SCBAR_SIZE - 1) * nsize);
                    notifyLine = Math.min(nsize - lineLimit, Math.max(0, notifyLine));
                }
                notifyScrollDragged = false;
                return;
            }
        }
    }
    
    @Override
    public void mouseHeld(int button, int mx, int my)
    {
        
    }

    @Override
    public void keyPressed(int keyCode, int keyChar)
    {
        switch (keyCode)
        {
        case Keyboard.KEY_ESCAPE:
            parent.stop();
            break;
        case Keyboard.KEY_M:
            gameState.getCurrentLocation().getAvailableQuests().add(
                    new Quest(gameState.getQuest("q_test1"), QuestStatus.AVAILABLE));
            break;
        case Keyboard.KEY_P:
            showTradeDialog(gameState.getWorldMap().getTown("NUS"));
            break;
        case Keyboard.KEY_O:
            actionPerformed(PopupDialog.ACTION_DIALOG_CLOSED);
            break;
        }
        if (dialog != null)
        {
            dialog.keyPressed(keyCode, keyChar);
            return;
        }
        /* For rest of key commands, ignore if sprite is moving */
        if (playerSprite.isMoving())
        {
            return;
        }
        switch (keyCode)
        {
        case Keyboard.KEY_W:
            movePressed(GlobalData.DIR_UP);
            break;
        case Keyboard.KEY_S:
            movePressed(GlobalData.DIR_DOWN);
            break;
        case Keyboard.KEY_A:
            movePressed(GlobalData.DIR_LEFT);
            break;
        case Keyboard.KEY_D:
            movePressed(GlobalData.DIR_RIGHT);
            break;
        case Keyboard.KEY_Q:
            questPressed();
            break;
        case Keyboard.KEY_R:
            searchPressed();
            break;
        case Keyboard.KEY_H:
            hidePressed();
            break;
        case Keyboard.KEY_L:
            laagerPressed();
            break;
        case Keyboard.KEY_1:
            supermarketPressed();
            break;
        case Keyboard.KEY_2:
            workshopPressed();
            break;
        case Keyboard.KEY_3:
            kopitiamPressed();
            break;
        case Keyboard.KEY_4:
            facBranchPressed();
            break;
        }
    }

    @Override
    public void keyReleased(int keyCode, int keyChar)
    {
        if (dialog != null)
        {
            dialog.keyReleased(keyCode, keyChar);
            return;
        }
    }
    
    @Override
    public void actionPerformed(int action)
    {
        switch(action)
        {
        case PopupDialog.ACTION_DIALOG_CLOSED:
            dialog = null;
            break;
        case PopupDialog.ACTION_OK_PRESSED:
            if (dialog != null && dialog instanceof TradeDialog)
            {
                String note = ((TradeDialog)dialog).getCloseNotification();
                if (note != null)
                {
                    addNotification(note, NOTIFY_TRADE);
                }
            }
            dialog = null;
            break;
        }
    }
    
    /**
     * Gets the color for each category of notification
     * @param category the notification category
     * @return the color corresponding to that category
     */
    private static Color notifyCatColor(int category)
    {
        switch (category)
        {
        case NOTIFY_NEWS:
            return NEWS_NOTIFICATION_COLOR;
        case NOTIFY_QUEST:
            return QUEST_NOTIFICATION_COLOR;
        case NOTIFY_TRADE:
            return TRADE_NOTIFICATION_COLOR;
        default:
                return Color.black;
        }
    }

    @Override
    public int getMouseX()
    {
        return parent.getMouseX();
    }

    @Override
    public int getMouseY()
    {
        return parent.getMouseY();
    }

    @Override
    public boolean isMouseButtonDown(int button)
    {
        return Mouse.isButtonDown(button);
    }

}
